# Copyright 2011-2011 by iNeed,Inc. All Rights Reserved.
#-*- encoding:utf-8 -*-
"""
MC2CSP (mobile c2c service protocol)
version 1.0

1,LOGIN
Description:
    users log in to server via LOGIN method
example:
    LOGIN MC2CSP/1.0
    Location: (60,15,"GPS:fake passenger Address")
    Time: Mon, 17 Oct 2011 17:20:05
    CSeq: 1
    From: 18917900803
    To: hxy20100114
    Role: 1
    Authorization: port=30803,pwd=fdd
    END_MC2CSP

    MC2CSP/1.0 LOGIN 200 OK
    Session: 3007
    Time: Mon, 17 Oct 2011 17:20:06
    CSeq: 1
    END_MC2CSP


2, PUBLISH
Description:
   users publish service
example:
    PUBLISH MC2CSP/1.0
    Location: (340,353,"GPS:fake passenger Address")
    Time: Mon, 17 Oct 2011 17:20:09
    Session: 3007
    CSeq: 2
    From: 18917900803
    To: hxy20100114
    Service: taxi
    departureLocation: (340, 353, 'Shanghai South Station')
    departureTime: Mon, 17 Oct 2011 17:35:09
    timeout: 60
    destinationLocation: (640, 320, 'Xujiehui Square')
    status: booking
    END_MC2CSP

    MC2CSP/1.0 PUBLISH 200 OK
    Time: Mon, 17 Oct 2011 17:20:09
    Session: 3007
    PubID: 457
    CSeq: 2
    END_MC2CSP

3, UNPUBLISH
Description:
   users unpublish service
example:
   UNPUBLISH MC2CSP/1.0
   Location: (9896,5421,"NO.120 East Guilin St.")
   Time: Thu, 21 Apr 2011 11:01:07 GMT
   From: 86-13917841776
   To: hxy20100114                                              #this indicates the msg center
   Session: 1024
   PubID: 1                                                     #subscription ID, one session may has a lot of subscriptions
   CSeq: 3
   END_MC2CSP

   MC2CSP/1.0 UNPUBLISH 200 OK
   Time: Thu, 21 Apr 2011 11:01:10 GMT
   Session: 1024
   PubID: 1
   CSeq: 3
   END_MC2CSP
6, NOTIFY

Description:
   peer notify peer, herein, peer may be server or client
   By far, there are 6 kinds of notify info
   1,check availability
   2,confirm order with passenger
   3,confirm order with driver
   4,get peer postion
   5,notify driver status
   6,transaction begin
example:
#dirver->center updateDriverStatus

    NOTIFY MC2CSP/1.0
    Location: (215,136,"GPS:fake taxi Address")
    Time: Mon, 17 Oct 2011 17:42:01
    Session: 3002
    CSeq: 3
    From: 13917841776
    To: hxy20100114
    Action: updateDriverStatus
    status: processing
    END_MC2CSP

    MC2CSP/1.0 NOTIFY 200 ok
    Location: (0,0,"iNeed Global Center")
    Time: Mon, 17 Oct 2011 17:42:01
    Session: 3002
    CSeq: 3
    Action: updateDriverStatus
    END_MC2CSP

#server->driver checkAvilability

    NOTIFY MC2CSP/1.0
    Location: (0,0,"iNeedTech Global Center")
    Time: Mon, 17 Oct 2011 17:20:09
    Session: 3008
    CSeq: 1
    From: hxy20100114
    To: 13917841776
    Action: checkAvailability
    pubID: 457
    departureLocation: (340, 353, 'Shanghai South Station')
    departureTime: Mon, 17 Oct 2011 17:35:09
    destinationLocation: (640, 320, 'Xujiehui Square')
    END_MC2CSP

    MC2CSP/1.0 NOTIFY 200 ok
    Location: (129,102,"GPS:fake taxi Address")
    Time: Mon, 17 Oct 2011 17:20:11
    Session: 3008
    CSeq: 1
    Action: checkAvailability
    PubID: 457
    END_MC2CSP

#server->passenger

   NOTIFY MC2CSP/1.0
   Location: (0,0,"center address)
   Time: Thu, 21 Apr 2011 11:01:07
   From: hxy20100114
   To: 13817794778
   Session: 1023
   CSeq: 4
   Action: confirmOrder
   pubID=3023
   departureTime=Thu, 21 Apr 2011 11:01:07
   departureLocation=(7896,3421,"NO.775 Pusan RD")
   destinationLocation=(7896,3421,"NO.948 Lujiazui ring RD")
   destinationTime=Thu, 21 Apr 2011 11:31:07              #option
   price=30-40
   carID=8063
   carBrand=SanTana
   carColor=white
   driverID=13917841776
   driverName=Diane Fan
   driverGender=Female
   driverIP=127.0.0.1:4236
   passengerID=13817794778
   passengerName=Number Huang
   passengerGender=Male

   MC2CSP/1.0 200 OK
   Location: (7896,3421,"NO.775 Pusan RD")
   Time: Thu, 21 Apr 2011 11:01:10
   Session: 1023
   CSeq: 4
   pubID=3023
   Action: confirmOrder

Description:
   peer confirmDriver
example:
#the passenger invite the driver
    NOTIFY MC2CSP/1.0
    Location: (340,353,"GPS:fake passenger Address")
    Time: Mon, 17 Oct 2011 17:20:14
    CSeq: 1
    From: 18917900803
    To: 13917841776
    Action: confirmDriver
    pubID: 457
    departureLocation: (340, 353, 'Shanghai South Station')
    departureTime: Mon, 17 Oct 2011 17:35:09
    destinationLocation: (640, 320, 'Xujiehui Square')
    driverName: Danie Fan
    driverGender: 0
    driverID: 13917841776
    licenseID: 400705
    company: Bus-taxi
    carID: 2005
    carBrand: Polo
    carColor: red
    passengerID: 18917900803
    passengerName: Tiexin Huang
    passengerGender: 1
    END_MC2CSP

    MC2CSP/1.0 NOTIFY 200 ok
    Location: (133,119,"GPS:fake taxi Address")
    Time: Mon, 17 Oct 2011 17:20:16
    CSeq: 1
    Action: confirmDriver
    PubID: 457
    END_MC2CSP


Description:
   getPeerPosition
example:

    NOTIFY MC2CSP/1.0
    Location: (340,353,"GPS:fake passenger Address")
    Time: Mon, 17 Oct 2011 17:20:16
    CSeq: 2
    From: 18917900803
    To: 13917841776
    Action: getPeerPosition
    END_MC2CSP

    MC2CSP/1.0 NOTIFY 200 ok
    Location: (136,122,"GPS:fake taxi Address")
    Time: Mon, 17 Oct 2011 17:20:16
    CSeq: 2
    Action: getPeerPosition
    END_MC2CSP

Description:
   transaction begin, this is a draft currently
example:
    NOTIFY MC2CSP/1.0
    Location: (320,330,"GPS:fake passenger Address")
    Time: Mon, 17 Oct 2011 17:20:24
    CSeq: 29
    From: 18917900803
    To: 13917841776
    Action: beginTansaction #we need to add the tansaction info,like price in the info
    pubID: 1023
    END_MC2CSP

    MC2CSP/1.0 NOTIFY 200 ok
    Location: (299,306,"GPS:fake taxi Address")
    Time: Mon, 17 Oct 2011 17:20:24
    CSeq: 29
    Action: beginTansaction
    PubID: 1023
    END_MC2CSP

8,LOGOUT
Description:
   users log out,and all pending subscription/publish are deleted
example:
   LOGOUT MC2CSP/1.0
   Location: (7896,3421,"NO.775 Pusan RD")                  #(x,y,name)
   Time: Thu, 21 Apr 2011 11:01:07
   From: 86-13817794778
   To: hxy20100114
   Session: 1023                                        #this indicates the msg center
   CSeq: 8

   MC2CSP/1.0 LOGOUT 200 OK
   Time: Thu, 21 Apr 2011 11:01:10
   Session: 1023
   CSeq: 8


"""
import sys, os, time, string,re

import socket
import database
import sock_asyncore
import msgHandler,gridManager
from common import *
import logging,traceback
from sessionPeer import SessionPeer
__author__  = "Number Huang<huang.bafeng@iNeed.com>"
__status__  = "developing"
__version__ = "0.1108.13.1"
_maxListner = 64
msgLogFilter=(
"Action: updateDriverStatus",
"Action: getPeerPosition",
)
def FilterMsgLog(info):
    if info:
        for log in msgLogFilter:
            if info.find(log)>=0:
                return True
    return False
class MsgProxy(sock_asyncore.channelManager):
    """
    MsgProxy is a session manager, who responsible for the lifecycle of the sessions.
    """

    def __init__(self,owner):

       super(MsgProxy,self).__init__(owner)
       self.owner = owner
       self.log = owner.log
       self.sessionList=[]

    def _createSession(self,cls,sock = None):

        if sock==None:
            sock = sock_asyncore.CreateSock(socket.AF_INET, socket.SOCK_STREAM,self)
        session = cls(self,sock,self.owner)
        self.add_channel(session)
        self.sessionList.append(session)
        return session

    def createClientSession(self,addr=None):
        try:
            session = None#session may not exist if crash in createSession,so set it before create
            session = self._createSession(Session)
            if addr:
                session.connect(addr)
                session.peer.setIP(addr[0],addr[1])
                
        except Exception,e:
            #errInfo = "createClientSession fail: %s"%e
            if session:#session may not exist if crash in createSession
                self.handle_error(session,e)
            self.log.exception("ip=%s"%str(addr))
            raise
        return session

    def createListner(self,addr):

        try:
            session = None#session may not exist if crash in createSession,so set it before create
            session = self._createSession(Listner)
            session.bind(addr)
            session.listen(_maxListner)
        except Exception,e:
            errInfo = "createListner fail: %s"%e
            self.log.exception(errInfo)
            if session:
                self.handle_error(session,e)
            raise
        else:
            self.log.debug("listening at %s:%d"%addr)
            return session


    def _closeSession(self,session):
        self.del_channel(session)
        if session in self.sessionList:
            self.sessionList.remove(session)
        session.exit()

    def handle_error(self,channel,exception):
        """
        all the underlying error of the channel will be throwed to here,
        and the proxy/manager will handle them
        """
        self._closeSession(channel)
        self.log.error("%s"%exception)


    def handle_close(self,channel):
        """
        channel will not close itself, rather,
        the close request will throw to the proxy/manager
        """
        self._closeSession(channel)

    def getSessionByPhone(self,phone):

        for session in self.sessionList:
            peerPhone = session.peer.getPhone()
            if peerPhone:
                if peerPhone==phone:
                    return session
                #self.log.debug("curPhone=%s,expect=%s"%(session.peer.getPhone(),phone))
        return None

class Listner(sock_asyncore.channel):
    """
        As its name implicit, it is a socket listner
    """

    def __init__(self, msgProxy,sock,owner):

        super(Listner,self).__init__(msgProxy,sock)
        self.set_reuse_addr()
        self.owner = owner
        self.phone= 0
        self.peer = SessionPeer(self)

    def exit(self):
        self.close()

    def handle_accept(self):

        #self.log.debug("beging accept")
        try:
            newSocket, addr = self.accept()
        except Exception,e:
            errInfo = "accept fail: %s"%e
            self.log.error(errInfo)
            self.log.error("%s"%traceback.format_exc())
            return
        else:
            session = self.manager._createSession(Session,newSocket)
            session.handle_connect()
            session.peer.setIP(addr[0],addr[1])
           
            self.log.debug("Connected from %s:%d"%addr)

requestName=re.compile(r"(.+) MC2CSP/1.0")
respondName=re.compile(r"MC2CSP/1.0 (.+) \d.+")
pattern1=(requestName,respondName)
MAX_PACKSIZE = 8192
                
class Session(sock_asyncore.channel):
    """
        Session represents the connection between two peers and act as the wrapper of socket.
        And it is also the bridge between the peer and msgHandler by notifyResponse/notifyRequest/
        sendRequest/sendData.
        It creates the msgHandler in the fly when needed.
    """

    def __init__(self, msgProxy,sock,owner,responseTimeout=10):
        """
        @type owner: the peer who owns the msgProxy
        """
        super(Session,self).__init__(msgProxy,sock)
        
        self.inBuf = ""
        

        self.msgHandlerMap = {} #item=methodName:(requester,responser)
        self.cseq = 0 #invalid cseq
        self.sessionID = 0 #invalid id
        self.responseMap={} #value=(retCode,retInfo,extraInfo,reporter,handler)
        #self.standbyDic={}
        self.owner = owner
        self.msgEngine = owner.getMsgEngine()
        self.responseTimeout = responseTimeout
       
        self.peer  = SessionPeer(self)#@because the peer is related to grid
        self.pubID = 0
        #task owned by the session should be managed by the session, otherwise the task will not be cleared if the session exit by exception
        self.taskList=[]
        self.writelock = threading.Condition()
        self.readlock  = threading.Condition()
    
    def exit(self):
        """
        do not call it directly, rather, call handle_close, which will ask its manager to close it
        """
        for engine,task in self.taskList:
            self.log.debug("task_%s in the closing session"%task.name)
            engine.deleteTask(task)
        self.close()

    
        

    def handleMsg(self,buf):
        """
        get the msgHandler according to the msg type
        """
        
        idx = buf[:32].find("MC2CSP")
        isResponse = idx==0
        m = pattern1[isResponse].search(buf)
        try:

            method = m.group(1)
            #got request, then find the responser,vice versa
            handler = self._getMsgHandler(method,IsGetResponser=not isResponse)
            handler.handle(buf)

        except Exception,e:
            self.log.exception("handlMsg Error")
    
    def setPubID(self,id):
        self.pubID = id 
    
    def setSessionID(self,id):
        self.sessionID = id

    def createSessionID(self):
        self.sessionID = self.owner.getNextSessionID()
        return self.sessionID

    def getNextCSeq(self):
        return self.cseq + 1

    def getPeer(self):
        return self.peer

    def getCurLocation(self):

        return self.owner.getLocation()

    def sendStandyResponse(self,method,standbyFields,retCode,retInfo):

        buf2 = "MC2CSP/1.0 %s %d %s\r\n"%(method,retCode,retInfo)
        buf2 += "Location: (%d,%d,\"%s\")\r\n"%self.getCurLocation()
        buf2 += getDefaultTime()
        buf2 += standbyFields + TERMINATOR
        self.sendData(buf2)

    def sendRequest(self,method,params,reporter,location,flag=0):
        """

        @type reporter: It can be None, because some caller do not need the result.
                        If reporter.async (by default) is set, the response will
                        be async and notify the caller via the callback handler.
                        Else, it is sync response,caller will use pollResponse by cseq
                        to sync the response
        """
        with AutoLock(self,self.writelock):#,"sr:%d-%d-%s"%(self.sessionID,self.cseq+1,method)):#this should be set thread safe

            self.cseq += 1

            if not reporter:
                reporter = NonReporter #NULL Pattern is a good design
                reporter.async = False
            if (flag&FLG_NORESPONSE)==0:
                self.responseTimeout = reporter.timeout
                handler = self._getMsgHandler(method,IsGetResponser=0)
                handler.reSchedule(self.responseTimeout)#reschedule the task
                self.msgEngine.addTask(handler,self)
            else:
                handler = None

            buf = "%s MC2CSP/1.0\r\n"%method
            buf  +="Location: (%d,%d,\"%s\")\r\n"%location
            #if not time:
            time = getLocalTime()
            buf +="Time: %s\r\n"%time

            if(self.sessionID):
                buf += "Session: %d\r\n"%self.sessionID
            buf += "CSeq: %d\r\n"%self.cseq
            buf += params
            buf += TERMINATOR

            self.responseMap[self.cseq] = [None,None,None,reporter,handler]
            self.sendData(buf)
            return self.cseq

    def pollResponse(self, cseq):
        """
        caller will
        use pollResponse by cseq to sync the response
        """
        return self.responseMap[cseq][:3]

    def notifyRequest(self,method,params):

        return self.owner.handleRequest(self,method,params)

    def notifyResponse(self,cseq,retCodeInt,retInfoStr,extraInfoMap):
        """
        The function ,unlike notifyRequest, will not interupt the owner directly
        Rather, it uses the reporter for async process or the responseMap for sync process
        @type cseq: In async mode, it is possible that the true response arrives after timeout,
                    so, let's use the cseq as a flag to indicate if the response is a garbage
        """
        if self.responseMap.has_key(cseq):
            self.responseMap[cseq][:3] = retCodeInt,retInfoStr,extraInfoMap
            reporter = self.responseMap[cseq][3]
            if reporter.async:
                handler = self.responseMap[cseq][4]
                self.msgEngine.deleteTask(handler)
                reporter.notify(retCodeInt,retInfoStr,extraInfoMap)
                del self.responseMap[cseq]
        else:
            self.log.debug("got the response %d,%d after timeout,ret=(%d,%s)"%(self.sessionID,cseq,retCodeInt,retInfoStr))

    def sendData(self,buf):
        
        with AutoLock(self,self.writelock):#,"sr:data%d-%d"%(self.sessionID,self.cseq)):#this should be set thread safe
            if(not self.checkWriteable(2)):
                raise Exception,"unwritable for 2 seconds"
            if not FilterMsgLog(buf):
                self.log.debug("\nsending...\n%s"%buf)
            try:
                
                ret = self.send(buf)
                
            except Exception, why:
                self.handle_error(why)
                self.log.exception(why)
        
    
    def recvData(self,size=MAX_PACKSIZE):
        """
        because the recvData is triger by select, so unlike write,it is not necessary to check readable any more
        """
        
        try:
            buf = self.recv (size)
            if not FilterMsgLog(buf):
                self.log.debug("\n%s"%buf)
            return buf
        except:# socket.error, why:
            #self.handle_error(why)
            raise #raise to the up level to process
       
        
    def handle_read (self):
        
        with AutoLock(self,self.readlock):#this should be set thread safe
            self.inBuf = self.inBuf + self.recvData()
            if self.inBuf:
                terminator_len = len(TERMINATOR)
                index = self.inBuf.find(TERMINATOR)
                if index != -1:
                    self.handleMsg(self.inBuf[:index])
                     
                    #memmove the remained data 
                    self.inBuf = self.inBuf[index+terminator_len:]    
       
    def _createHandler(self,method,IsGetResponser):
        """
        hanlder factory
        """
        if not IsGetResponser:
            cls = getattr(msgHandler,method+"Requester")
            handler = cls(self,self.responseTimeout)
        else:
            cls = getattr(msgHandler,method+"Responser")
            handler = cls(self)

        return handler

    def _addMsgHandler(self,method,handler,IsGetResponser):

        self.msgHandlerMap[method][IsGetResponser] = handler

    def _getMsgHandler(self,method,IsGetResponser):

        if not self.msgHandlerMap.has_key(method):
            handler = self._createHandler(method,IsGetResponser)
            self.msgHandlerMap[method]=[None,None]
            self._addMsgHandler(method,handler,IsGetResponser)
        else:
            handler = self.msgHandlerMap[method][IsGetResponser]
            if not handler:
                handler = self._createHandler(method,IsGetResponser)
                self._addMsgHandler(method,handler,IsGetResponser)


        return handler

    def _delMsgHandler(self,method):
        """
        Not used so far
        """
        del self.msgHandlerMap[method]

if __name__=="__main__":

        msg="'%s'"%"what"
        print msg

